Client Tools
Client Tools are tools provided by the client application (IDE extension, web UI, mobile app) at runtime. They enable human-in-the-loop interactions where the agent requests actions that the client executes.
What Are Client Tools?
Unlike C# tools (server-side) or MCP servers (external processes), Client Tools are:
- Defined by the client at runtime
- Executed by the client (not the server)
- Human-in-the-loop - the user sees and controls what happens
Agent: "I need to open a file"
│
▼
Client Tool Request ──► Client Application
│ │
│ User sees action
│ │
◄─────────────────── Tool Result
│
Agent: "File opened successfully"Common use cases:
- Open files in an IDE
- Show dialogs to the user
- Get user's current selection
- Navigate to URLs
- Display previews
Quick Start
1. Enable Client Tools
var agent = new AgentBuilder()
.WithClientTools()
.Build();2. Provide Tools at Runtime
Tools are provided through AgentRunInput:
var runInput = new AgentRunInput
{
ClientToolGroups = new[]
{
new ClientToolGroupDefinition(
Name: "IDE",
Description: "IDE interaction tools",
Tools: new[]
{
new ClientToolDefinition(
Name: "OpenFile",
Description: "Open a file in the editor",
ParametersSchema: JsonDocument.Parse(@"{
""type"": ""object"",
""properties"": {
""path"": { ""type"": ""string"", ""description"": ""File path"" }
},
""required"": [""path""]
}").RootElement
)
}
)
}
};
await foreach (var evt in agent.RunAsync("Open the config file", thread, runInput))
{
// Handle events...
}3. Handle Tool Invocations
Listen for ClientToolInvokeRequestEvent and respond:
await foreach (var evt in agent.RunAsync(message, thread, runInput))
{
if (evt is ClientToolInvokeRequestEvent request)
{
// Execute the tool (client-side)
var result = await ExecuteToolAsync(request.ToolName, request.Arguments);
// Send response back
await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
RequestId: request.RequestId,
Content: new[] { new TextContent(result) },
Success: true
));
}
}Tool Group Definition
Client tools are organized into tool groups:
public record ClientToolGroupDefinition(
string Name, // Tool group identifier
string? Description, // Shown when collapsed
IReadOnlyList<ClientToolDefinition> Tools,
IReadOnlyList<ClientSkillDefinition>? Skills = null,
string? FunctionResult = null, // One-time message on expansion
string? SystemPrompt = null, // Persistent instructions
bool StartCollapsed = true // Hide behind container initially
);Tool Definition
public record ClientToolDefinition(
string Name, // Unique tool name
string Description, // Shown to agent
JsonElement ParametersSchema, // JSON Schema for parameters
bool RequiresPermission = false // Require user approval
);Example: IDE Tool Group
new ClientToolGroupDefinition(
Name: "IDE",
Description: "VS Code interaction tools",
StartCollapsed: true,
// Auto-generated: "IDE expanded. Available functions: OpenFile, GetSelection, ShowMessage"
FunctionResult: null, // Auto-generated message is sufficient
SystemPrompt: "Use ShowMessage for important notifications to the user.",
Tools: new[]
{
new ClientToolDefinition(
Name: "OpenFile",
Description: "Open a file in the editor",
ParametersSchema: ParseSchema(@"{
""type"": ""object"",
""properties"": {
""path"": { ""type"": ""string"" },
""line"": { ""type"": ""integer"" }
},
""required"": [""path""]
}")
),
new ClientToolDefinition(
Name: "GetSelection",
Description: "Get the user's current text selection",
ParametersSchema: ParseSchema(@"{ ""type"": ""object"" }")
),
new ClientToolDefinition(
Name: "ShowMessage",
Description: "Show a message to the user",
ParametersSchema: ParseSchema(@"{
""type"": ""object"",
""properties"": {
""message"": { ""type"": ""string"" },
""severity"": { ""type"": ""string"", ""enum"": [""info"", ""warning"", ""error""] }
},
""required"": [""message""]
}")
)
}
)Collapsing
By default, client tool groups start collapsed (StartCollapsed = true). This groups all tools under a container:
Before expansion: After expansion:
┌──────────────────┐ ┌──────────────────┐
│ IDE │ ──► │ OpenFile │
│ (3 tools) │ │ GetSelection │
└──────────────────┘ │ ShowMessage │
└──────────────────┘Control Collapsing Globally
var config = new AgentConfig
{
Collapsing = new CollapsingConfig
{
CollapseClientTools = true, // Enable collapsing
ClientToolsInstructions = "These tools interact with the user's IDE."
}
};Control Per-Group
new ClientToolGroupDefinition(
Name: "CriticalTools",
StartCollapsed: false, // Always visible
Tools: ...
)Pre-Expand Tool Groups
var runInput = new AgentRunInput
{
ClientToolGroups = toolGroups,
ExpandedContainers = new HashSet<string> { "IDE" } // Start expanded
};Instructions
Provide guidance when tool groups expand using dual-context architecture:
| Parameter | Location | Lifetime | Use For |
|---|---|---|---|
FunctionResult | Conversation history | One-time on expansion | Additional context (appended to auto-generated message) |
SystemPrompt | System prompt | Every turn while expanded | Critical rules, workflow |
Important: The system automatically generates a base expansion message:
"{ToolGroupName} expanded. Available functions: {FunctionList}"Your
FunctionResultis appended to this auto-generated message. Don't duplicate the expansion info—use it only for additional context. Passnullif the auto-generated message is sufficient.
FunctionResult (One-Time, Appended)
new ClientToolGroupDefinition(
Name: "IDE",
// Auto-generated: "IDE expanded. Available functions: OpenFile, GetSelection, ShowMessage"
FunctionResult: "Tip: Use GetSelection before making edits.", // Additional context only
...
)SystemPrompt (Persistent)
new ClientToolGroupDefinition(
Name: "IDE",
SystemPrompt: @"
IDE RULES:
- Always confirm before modifying files
- Use ShowMessage for important notifications
- Check selection before applying edits",
...
)Global Client Tool Instructions
var config = new AgentConfig
{
Collapsing = new CollapsingConfig
{
ClientToolsInstructions = "These tools interact with the user. Be respectful of their time."
}
};Handling Tool Invocations
Request Event
When the agent calls a client tool:
public record ClientToolInvokeRequestEvent(
string RequestId, // Correlation ID (must match in response)
string ToolName, // Which tool to execute
string CallId, // LLM's function call ID
IReadOnlyDictionary<string, object?> Arguments,
string? Description
);Response Event
Send back the result:
public record ClientToolInvokeResponseEvent(
string RequestId, // Must match request
IReadOnlyList<IToolResultContent> Content,
bool Success = true,
string? ErrorMessage = null,
ClientToolAugmentation? Augmentation = null
);Result Content Types
// Text result
new TextContent("File opened successfully")
// JSON result
new JsonContent(JsonDocument.Parse(@"{ ""line"": 42, ""column"": 10 }").RootElement)
// Binary result (file, image, etc.)
new BinaryContent(
MimeType: "image/png",
Data: base64EncodedData,
Filename: "screenshot.png"
)Example Handler
await foreach (var evt in agent.RunAsync(message, thread, runInput))
{
switch (evt)
{
case ClientToolInvokeRequestEvent request:
try
{
var result = request.ToolName switch
{
"OpenFile" => await OpenFileAsync(request.Arguments["path"]?.ToString()),
"GetSelection" => await GetSelectionAsync(),
"ShowMessage" => await ShowMessageAsync(request.Arguments),
_ => throw new NotSupportedException($"Unknown tool: {request.ToolName}")
};
await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
RequestId: request.RequestId,
Content: new[] { new TextContent(result) },
Success: true
));
}
catch (Exception ex)
{
await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
RequestId: request.RequestId,
Content: Array.Empty<IToolResultContent>(),
Success: false,
ErrorMessage: ex.Message
));
}
break;
case TextDeltaEvent delta:
Console.Write(delta.Text);
break;
}
}Dynamic State Changes (Augmentation)
Tool responses can modify the agent's state:
await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
RequestId: request.RequestId,
Content: new[] { new TextContent("User logged in") },
Augmentation: new ClientToolAugmentation
{
// Add new tool groups
InjectToolGroups = new[] { adminToolGroup },
// Remove tool groups
RemoveToolGroups = new HashSet<string> { "LoginTools" },
// Control expansion
ExpandToolGroups = new HashSet<string> { "AdminTools" },
CollapseToolGroups = new HashSet<string> { "GuestTools" },
// Control visibility
HideTools = new HashSet<string> { "Login" },
ShowTools = new HashSet<string> { "Logout" },
// Update context
AddContext = new[] { new ContextItem("user", "Admin user logged in") }
}
));Configuration
var agent = new AgentBuilder()
.WithClientTools(config =>
{
config.InvokeTimeout = TimeSpan.FromSeconds(30);
config.DisconnectionStrategy = ClientDisconnectionStrategy.FallbackMessage;
config.MaxRetries = 3;
config.FallbackMessageTemplate = "Client disconnected. Tool '{0}' unavailable.";
config.ValidateSchemaOnRegistration = true;
})
.Build();Disconnection Strategies
| Strategy | Behavior |
|---|---|
FailFast | Throw exception immediately |
RetryWithBackoff | Retry up to MaxRetries times |
FallbackMessage | Return error message to agent (default) |
Client Skills
Group tools into workflows with dual-context instructions:
new ClientToolGroupDefinition(
Name: "Checkout",
Tools: new[] { validateCart, processPayment, confirmOrder },
Skills: new[]
{
new ClientSkillDefinition(
Name: "Complete Checkout",
Description: "Process a customer's checkout",
// Auto-generated: "Complete Checkout skill activated. Available functions: ValidateCart, ProcessPayment, ConfirmOrder"
FunctionResult: null, // Auto-generated message is sufficient
SystemPrompt: @"
CHECKOUT WORKFLOW:
1. Validate the cart contents
2. Process payment
3. Confirm order with customer
4. Show confirmation message",
References: new[]
{
new ClientSkillReference("ValidateCart"),
new ClientSkillReference("ProcessPayment"),
new ClientSkillReference("ConfirmOrder")
}
)
}
)State Persistence
Client state persists across message turns by default:
// Turn 1: Register tool groups
var runInput1 = new AgentRunInput { ClientToolGroups = toolGroups };
await agent.RunAsync("Hello", thread, runInput1);
// Turn 2: Tool groups still registered (state persists)
await agent.RunAsync("Use the IDE tools", thread);
// Turn 3: Reset state
var runInput3 = new AgentRunInput { ResetClientState = true };
await agent.RunAsync("Start fresh", thread, runInput3);Best Practices
Use collapsing for tool groups with many tools to reduce context clutter.
Provide clear descriptions so the agent knows when to use each tool.
Handle errors gracefully - always send a response, even on failure.
Use RequiresPermission for tools that modify user data.
Keep tools focused - one action per tool.
// Good: Focused tools with clear purposes
new ClientToolDefinition(
Name: "OpenFile",
Description: "Open a file in the editor at an optional line number",
RequiresPermission: false,
ParametersSchema: ...
)
new ClientToolDefinition(
Name: "DeleteFile",
Description: "Permanently delete a file",
RequiresPermission: true, // Destructive action
ParametersSchema: ...
)Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Tool not appearing | Tool group collapsed | Set StartCollapsed: false or pre-expand |
| Timeout error | Client not responding | Increase InvokeTimeout |
| Schema validation error | Invalid JSON Schema | Check schema syntax |
| Response not received | RequestId mismatch | Ensure RequestId matches |